package viper.receiver;

import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;

import viper.main.StateManager;
import viper.main.ViperHeader;
import viper.sender.UDPSender;

public class TCPReceiver implements Runnable {
	
	private final int MAX_BUFFER_SIZE = 50; //arbitrary, needs to be long enough to hold longest possible setup packet
	
	private ServerSocket servsock; //server socket
	private Socket csock; //client socket
	private DataInputStream dis;
	private DataOutputStream dos;
	private boolean isInitiator, running, loop;
	private int breakCount;
	
	public TCPReceiver(DataInputStream is){
		servsock = null;
		csock = null;
		dis = null;
		dos = null;
		
		running = true;
		loop = true;
		
		breakCount = 0;
		
		if(is==null)
			isInitiator = false;
		else{
			isInitiator = true;
			dis = is;
		}
	}
	
	public void run() {
		System.out.println("Starting TCPReceiver services.");
		if(!isInitiator){
			System.out.println("Listening for client requests.");
			try{
				servsock = new ServerSocket(StateManager.TCP_PORT);
			}catch(IOException ioe){
				System.out.println("Unable to listen on port "+StateManager.TCP_PORT);
				return;
			}
			System.out.println("Initialized ServerSocket.");

			csock = null;
			
			/*
			try{
				servsock.setSoTimeout(2000);
			}catch(SocketException se){
				System.out.println("SocketException in setting timeout at TCPReceiver.run().");
			}
			
			while(csock==null){ //keep checking for call requests until one is received
				try{
					csock = servsock.accept();
					if(!running){ //elegant exit when we want to stop listening for requests
						close();
						csock = new Socket(); //dummy socket to break loop
						loop = false;
					}
				}catch(IOException ioe){
					System.out.println("Unable to accept client connection.");
					return;
				}
			}
			*/
			
			try{
				csock = servsock.accept();
			}catch(IOException ioe){
				System.out.println("Unable to accept client connection.");
				return;
			}
			
			//go on with rest of code only if an end request wasn't issued.
			if(loop){
				System.out.println("Client request accepted.");
				InetAddress clientIP = csock.getInetAddress();

				System.out.println("Accepted client connection from "+clientIP.toString());

				dis = null;
				dos = null;
				try{
					dis = new DataInputStream(csock.getInputStream());
					dos = new DataOutputStream(csock.getOutputStream());
				}catch(IOException ioe){
					System.out.println("Unable to create i/o streams from client socket.");
					return;
				}
				System.out.println("Input/Output streams initialized.");

				int num = 0;

				//Continuously receive TCP data while program is running
				while(StateManager.INSTANCE.getState()!=StateManager.State.LOGGED_OUT && running){
					byte[] buffer = new byte[MAX_BUFFER_SIZE];
					try{
						num = dis.read(buffer);
						breakCount = 0;
					}catch(IOException ioe){
						if(breakCount>2){
							System.out.println("Force-break at other end, quitting.");
							end();
						}
						System.out.println("IOException in TCPReceiver.run().");
						breakCount++;
					}

					if(num>0){
						//extract relevant data
						byte[] toParse = new byte[num]; 
						System.arraycopy(buffer,0,toParse,0,num);

						//figure out what to do with the data
						if(toParse[0]==ViperHeader.CALL_SETUP)
							handleCallSetupPacket(((String)clientIP.toString()).substring(1,clientIP.toString().length()),toParse);
						else if(toParse[0]==ViperHeader.STATISTICS)
							handleStatisticPacket(toParse);
					}
				}
				System.out.println("Server TCPReceiver exited.");
				close(); //close all streams and sockets once an end is requested
				StateManager.INSTANCE.callEnded();
			}
		}
		else{
			System.out.println("Listening for server reply.");
			int num = 0;
			
			//Continuously receive TCP data while program is running
			while(StateManager.INSTANCE.getState()!=StateManager.State.LOGGED_OUT && running){
				byte[] buffer = new byte[MAX_BUFFER_SIZE];
				try{
					if(running) //redundant check here to prevent socket exceptions on late processed threads
						num = dis.read(buffer);
					breakCount = 0; //only reset counter if we had a successful read()
				}catch(IOException ioe){
					//if illegal force close on other end, just quit call
					if(breakCount>2){
						System.out.println("Force-break at other end, quitting.");
						end();
					}
					System.out.println("IOException in TCPReceiver.run().");
					breakCount++;
				}
				
				if(num>0){
					//extract relevant data
					byte[] toParse = new byte[num]; 
					System.arraycopy(buffer,0,toParse,0,num);

					//figure out what to do with the data
					if(toParse[0]==ViperHeader.CALL_SETUP)
						handleCallSetupPacket("",toParse); //we can put a blank for the IP because in this case, we are only calling, never receiving calls
					else if(toParse[0]==ViperHeader.STATISTICS)
						handleStatisticPacket(toParse);
				}
			}
			System.out.println("Client TCPReceiver exited.");
			if(breakCount>2)
				StateManager.INSTANCE.callEnded();
			//close(); //don't need here because dis is closed in StateManager
		}
	}
	
	public void end(){
		running = false;
	}
	
	public void close(){
		try{
			if(dos!=null)
				dos.close();
			if(dis!=null)
				dis.close();			
			if(csock!=null)
				csock.close();
			if(servsock!=null)
				servsock.close();
		}catch(IOException ioe){
			System.out.println("Unable to close i/o streams and sockets in TCPReceiver.end().");
			return;
		}
		System.out.println("Ended all TCPReceiver services.");
	}
	
	public boolean isClosed(){
		if(csock!=null && servsock!=null){
			
			/** DEBUG **
			if(!csock.isClosed())
				System.out.println("csock still open");
			if(!servsock.isClosed())
				System.out.println("servsock still open");
			**        **/
			
			if(csock.isClosed() && servsock.isClosed())
				return true;
			else
				return false;
		}
		else
			return true;
	}
	
	public DataInputStream getDataInputStream(){
		return dis;
	}
	
	public DataOutputStream getDataOutputStream(){
		return dos;
	}
	
	private void handleCallSetupPacket(String sourceAddrIP, byte[] pkt){
		switch(pkt[1]) {
			case ViperHeader.SETUP_MAKE_CALL: //somebody wants to talk
				//extract type of code and relay info
				byte[] payload = ViperHeader.getPayloadFromSetupPacket(pkt);
				byte typeCode = payload[0];
				int numRelays=0;
				if(typeCode==ViperHeader.CODE_111)
					numRelays = 1;
				else if(typeCode==ViperHeader.CODE_232)
					numRelays=3;
				else if(typeCode==ViperHeader.CODE_364)
					numRelays=6;
				String[] relays = new String[numRelays];
				for(int i=0; i<numRelays; i++)
				{
					byte[] ipBytes = new byte[]{
							payload[4*i + 1],
							payload[4*i + 2],
							payload[4*i + 3],
							payload[4*i + 4]
						};
					relays[i] = 
						UDPSender.ipBytesToInetAddress(ipBytes).getHostAddress();				}
				StateManager.INSTANCE.gotCall(sourceAddrIP, typeCode, relays);
				break;
			case ViperHeader.SETUP_ACCEPT_CALL: //person accepted your call
				StateManager.INSTANCE.callAccepted();
				break;
			case ViperHeader.SETUP_REJECT_CALL: //person rejected your call
				StateManager.INSTANCE.callRejected();
				break;
			case ViperHeader.SETUP_ERROR: //handle error
				break;
			case ViperHeader.SETUP_CANCEL_CALL: //call cancelled
				StateManager.INSTANCE.callCancelled();
				break;
			case ViperHeader.SETUP_END_CALL: //call ended
				if(isInitiator)
					StateManager.INSTANCE.callEnded();
				else{
					StateManager.INSTANCE.sendClientShutdown();
					end();
					//StateManager.INSTANCE.endCall();
				}
				break;
			default: 
				System.out.println("ignored call setup packet"); 
				break; 
		}
	}
	
	private void handleStatisticPacket(byte[] bytes){
		byte [] temp = new byte[bytes.length - 1]; //temporary byte[] to store the payload
		System.arraycopy(bytes, 1, temp, 0,bytes.length-1);
		long [] templong = new long[temp.length];
		templong[0] = (bytes[1] << 56)
		+ ((bytes[2] & 0xFF) << 48)
		+ ((bytes[3] & 0xFF) << 40)
		+ ((bytes[4] & 0xFF) << 32)
		+ ((bytes[5] & 0xFF) << 24)
		+ ((bytes[6] & 0xFF) << 16)
		+ ((bytes[7] & 0xFF) << 8)
		+ (bytes[8] & 0xFF);

		templong[1] = (bytes[9] << 56)
		+ ((bytes[10] & 0xFF) << 48)
		+ ((bytes[11] & 0xFF) << 40)
		+ ((bytes[12] & 0xFF) << 32)
		+ ((bytes[13] & 0xFF) << 24)
		+ ((bytes[14] & 0xFF) << 16)
		+ ((bytes[15] & 0xFF) << 8)
		+ (bytes[16] & 0xFF);
		//CONVERTTHESE TO LONGS
		StateManager.INSTANCE.getCallReceiver().setPacketsSent(templong);
	}
}
